using System;
using System.Collections.Generic;

namespace RikMigrations
{
    public class MigrationRunner
    {
        private readonly DbProvider dbProvider;
        private readonly Dictionary<string, List<MigrationAttribute>> migrationsFound;

        public MigrationRunner(DbProvider db, Dictionary<string, List<MigrationAttribute>> migrations)
        {
            dbProvider = db;
            migrationsFound = migrations;
        }

        public void MigrateTo(string[] modulesToMigrate, int? targetVersion)
        {
            foreach (string module in GetModulesToMigrate(modulesToMigrate))
            {
                if (migrationsFound.ContainsKey(module))
                {
                    int moduleTargetVersion = GetVersionToMigrateModuleTo(module, targetVersion);
                    while (moduleTargetVersion != dbProvider.GetModuleVersion(module))
                    {
                        int migrationVersionRan = 0;
                        foreach (MigrationAttribute migration in findNextMigrations(module, targetVersion))
                        {
                            if (moduleTargetVersion < dbProvider.GetModuleVersion(module))
                            {
                                migration.Down(dbProvider);
                                migrationVersionRan = migration.Version - 1;
                            }
                            else
                            {
                                migration.Up(dbProvider);
                                migrationVersionRan = migration.Version;
                            }
                            
                        }

                        if (migrationVersionRan == 0)
                            migrationVersionRan = dbProvider.GetModuleVersion(module)  + 1;

                        dbProvider.SetModuleVersion(module, migrationVersionRan);
                    }
                }
            }
        }

        private List<MigrationAttribute> findNextMigrations(string module, int? versionToMigrateModuleTo)
        {
            int currentModuleDbVersion = dbProvider.GetModuleVersion(module);
            int nextMigrationVersion = (versionToMigrateModuleTo < currentModuleDbVersion) ? currentModuleDbVersion : currentModuleDbVersion + 1;
            return migrationsFound[module].FindAll(delegate(MigrationAttribute migration) { return migration.Version == nextMigrationVersion; });
        }

        private int GetVersionToMigrateModuleTo(string module, int? versionToMigrateTo)
        {
            if (versionToMigrateTo == null)
                return getMaximumVersionNumber(migrationsFound[module]).Value;

            return versionToMigrateTo.Value;
        }

        private string[] GetModulesToMigrate(string[] modulesToMigrate)
        {
            string[] allModules = new string[migrationsFound.Count];
            if (noModulesSpecified(modulesToMigrate))
            {
                copyAllModulesFound(migrationsFound, allModules);
            }
            else
            {
                allModules = modulesToMigrate;
            }
            return allModules;
        }

        private static int? getMaximumVersionNumber(List<MigrationAttribute> moduleMigrations)
	    {
            moduleMigrations.Sort(CompareMigrationsByVersion);
	        return moduleMigrations[moduleMigrations.Count - 1].Version;
	    }

        private static int CompareMigrationsByVersion(MigrationAttribute migrationVersion1, MigrationAttribute migrationVersion2)
        {
            if (migrationVersion2.Version == migrationVersion1.Version)
                return 0;

            if (migrationVersion2.Version < migrationVersion1.Version)
                return 1;

            return -1;
        }

	    private static void copyAllModulesFound(Dictionary<string, List<MigrationAttribute>> migration, string[] allModules)
	    {
	        migration.Keys.CopyTo(allModules, 0);
	    }

	    private static bool noModulesSpecified(string[] modules)
	    {
	        return (modules == null) || (modules.Length == 0);
	    }
    }
}